/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.falko.android.raven.renderer;

import java.io.IOException;
import java.io.InputStream;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import com.falko.android.raven.util.AndroidUtil;

/**
 * CHANGES:
 * 	Moved the texture loading code to a utility class
 * 
 * The Texture Library manages all textures in the game. Textures are pooled and
 * handed out to requesting parties via allocateTexture(). However, the texture
 * data itself is not immediately loaded at that time; it may have already been
 * loaded or it may be loaded in the future via a call to loadTexture() or
 * loadAllTextures(). This allows Texture objects to be dispersed to various
 * game systems and while the texture data itself is streamed in or loaded as
 * necessary.
 */
public class TextureLibrary {
	// Textures are stored in a simple hash. This class implements its own
	// array-based hash rather
	// than using HashMap for performance.
	Texture[] mTextureHash;
	int[] mTextureNameWorkspace;
	int[] mCropWorkspace;
	static final int DEFAULT_SIZE = 512;
	static BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options();

	public TextureLibrary() {
		super();
		mTextureHash = new Texture[DEFAULT_SIZE];
		for (int x = 0; x < mTextureHash.length; x++) {
			mTextureHash[x] = new Texture();
		}

		mTextureNameWorkspace = new int[1];
		mCropWorkspace = new int[4];

		sBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
	}

	public void reset() {
		removeAll();
	}

	/**
	 * Creates a Texture object that is mapped to the passed resource id. If a
	 * texture has already been allocated for this id, the previously allocated
	 * Texture object is returned.
	 * 
	 * @param resourceID
	 * @return
	 */
	public Texture allocateTexture(int resourceID) {
		Texture texture = getTextureByResource(resourceID);
		if (texture == null) {
			texture = addTexture(resourceID, -1, 0, 0);
		}

		return texture;
	}

	/**
	 * Loads a single texture into memory. Does nothing if the texture is
	 * already loaded.
	 */
	public Texture loadTexture(Context context, GL10 gl, int resourceID) {
		Texture texture = allocateTexture(resourceID);
		texture = loadBitmap(context, gl, texture);
		return texture;
	}

	/**
	 * Loads all unloaded textures into OpenGL memory. Already-loaded textures
	 * are ignored.
	 */
	public void loadAll(Context context, GL10 gl) {
		for (int x = 0; x < mTextureHash.length; x++) {
			if (mTextureHash[x].resource != -1
					&& mTextureHash[x].loaded == false) {
				loadBitmap(context, gl, mTextureHash[x]);
			}
		}
	}

	/** Flushes all textures from OpenGL memory */
	public void deleteAll(GL10 gl) {
		for (int x = 0; x < mTextureHash.length; x++) {
			if (mTextureHash[x].resource != -1 && mTextureHash[x].loaded) {
				assert mTextureHash[x].name != -1;
				mTextureNameWorkspace[0] = mTextureHash[x].name;
				mTextureHash[x].name = -1;
				mTextureHash[x].loaded = false;
				gl.glDeleteTextures(1, mTextureNameWorkspace, 0);
				int error = gl.glGetError();

				assert error == GL10.GL_NO_ERROR;
			}
		}
	}

	/** Marks all textures as unloaded */
	public void invalidateAll() {
		for (int x = 0; x < mTextureHash.length; x++) {
			if (mTextureHash[x].resource != -1 && mTextureHash[x].loaded) {
				mTextureHash[x].name = -1;
				mTextureHash[x].loaded = false;
			}
		}
	}

	/**
	 * Loads a bitmap into OpenGL and sets up the common parameters for 2D
	 * texture maps.
	 */
	protected Texture loadBitmap(Context context, GL10 gl, Texture texture) {
		assert gl != null;
		assert context != null;
		assert texture != null;
		if (texture.loaded == false && texture.resource != -1) {
			
			InputStream is = context.getResources().openRawResource(
					texture.resource);
			Bitmap bitmap;
			try {
				bitmap = BitmapFactory.decodeStream(is);
			} finally {
				try {
					is.close();
				} catch (IOException e) {
					e.printStackTrace();
					// Ignore.
				}
			}
			
			AndroidUtil.loadTexture(gl, bitmap, texture);
			bitmap.recycle();
		}

		return texture;
	}

	public boolean isTextureLoaded(int resourceID) {
		return getTextureByResource(resourceID) != null;
	}

	/**
	 * Returns the texture associated with the passed Android resource ID.
	 * 
	 * @param resourceID
	 *            The resource ID of a bitmap defined in R.java.
	 * @return An associated Texture object, or null if there is no associated
	 *         texture in the library.
	 */
	public Texture getTextureByResource(int resourceID) {
		int index = getHashIndex(resourceID);
		int realIndex = findFirstKey(index, resourceID);
		Texture texture = null;
		if (realIndex != -1) {
			texture = mTextureHash[realIndex];
		}
		return texture;
	}

	private int getHashIndex(int id) {
		return id % mTextureHash.length;
	}

	/**
	 * Locates the texture in the hash. This hash uses a simple linear probe
	 * chaining mechanism: if the hash slot is occupied by some other entry, the
	 * next empty array index is used. This is O(n) for the worst case (every
	 * slot is a cache miss) but the average case is constant time.
	 * 
	 * @param startIndex
	 * @param key
	 * @return
	 */
	private int findFirstKey(int startIndex, int key) {
		int index = -1;
		for (int x = 0; x < mTextureHash.length; x++) {
			final int actualIndex = (startIndex + x) % mTextureHash.length;
			if (mTextureHash[actualIndex].resource == key) {
				index = actualIndex;
				break;
			} else if (mTextureHash[actualIndex].resource == -1) {
				break;
			}
		}
		return index;
	}

	/** Inserts a texture into the hash */
	protected Texture addTexture(int id, int name, int width, int height) {
		int index = findFirstKey(getHashIndex(id), -1);
		Texture texture = null;
		assert index != -1;

		if (index != -1) {
			mTextureHash[index].resource = id;
			mTextureHash[index].name = name;
			mTextureHash[index].width = width;
			mTextureHash[index].height = height;
			texture = mTextureHash[index];
		}

		return texture;
	}

	public void removeAll() {
		for (int x = 0; x < mTextureHash.length; x++) {
			mTextureHash[x].reset();
		}
	}

}
